package com.jxy.man_media.service;

import com.alibaba.fastjson.JSON;
import com.jxy.edu.common.exception.ExceptionCast;
import com.jxy.edu.common.model.response.CommonCode;
import com.jxy.edu.common.model.response.ResponseResult;
import com.jxy.edu.domain.media.MediaFile;
import com.jxy.edu.domain.media.response.CheckChunkResult;
import com.jxy.edu.domain.media.response.MediaCode;
import com.jxy.man_media.config.RabbitMQConfig;
import com.jxy.man_media.dao.MediaFileRepository;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;

@Service
public class MediaUploadService {
    @Autowired
    MediaFileRepository mediaFileRepository;

    @Value("${xc-service-manage-media.upload-location}")
    String upload_location;
    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    String routingkey_media_video;
    //路由Key
    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    private String routingkey;

    @Autowired
    RabbitTemplate rabbitTemplate;


    //得到文件所属目录路径
    private String getFileFolderPath(String fileMd5){
        //先通过进来的id,得到该文件的目录
        String mkdirPath = upload_location+fileMd5.substring(0,1)
                //subString(1,2),从1下标开始截取，2下标结束。
                +"/"+fileMd5.substring(1,2)+"/"+fileMd5;
        return mkdirPath;
    }
    //根据文件id和文件扩展名，得到文件的绝对路径
    private String getFilePath(String fileMd5,String fileExt){
        return upload_location + fileMd5.substring(0,1)
                + "/" + fileMd5.substring(1,2) + "/" + fileMd5
                + "/"+ fileMd5 + "." +fileExt;
    }


    //得到块文件所属目录路径
    private String getChunkFileFolderPath(String fileMd5){
        return  upload_location + fileMd5.substring(0,1)
                + "/" + fileMd5.substring(1,2)
                + "/" + fileMd5 + "/chunk/";
    }


    /**
     * 文件上传前的注册，检查文件是否存在
     * 根据文件md5得到文件路径
     * 规则：
     * 一级目录：md5的第一个字符
     * 二级目录：md5的第二个字符
     * 三级目录：md5
     * 文件名：md5+文件扩展名
     * @param fileMd5 文件md5值
     * @param fileExt 文件扩展名
     * @return 文件路径
     */
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        //1  检查文件在磁盘上是否存在
        //生成该文件需要创建的目录路径
        String fileFolderPath = this.getFileFolderPath(fileMd5);
        //得到文件绝对路径
        String filePath = this.getFilePath(fileMd5, fileExt);
        //判断，这个文件是否存在
        File file = new File(filePath);
        boolean exists = file.exists();

        //从mongo中取这个文件
        Optional<MediaFile> optionalFile = mediaFileRepository.findById(fileMd5);
        if (exists && optionalFile.isPresent()){
            //如果都本地和mongo中都有这个文件，证明上传过了，无需再次上传
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }

        //如果有数据库中没有。就判断，该文件的本地目录是否存在，不存在，就先创建该目
        File fileFolder = new File(fileFolderPath);
        if (!fileFolder.exists()){
            //如果没有这个目录就创建这个多级目录，mkdirs，加s是多级，不加是单个目录
            fileFolder.mkdirs();
        }
        //准备完毕，返回成功代码
        return new ResponseResult(CommonCode.SUCCESS);
    }
    /**
     *分块检查
     * @param fileMd5 文件唯一id
     * @param chunk   分块的下标
     * @param chunkSize  分块的大小
     * @return  返回是否存在
     */
    public CheckChunkResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize) {
        //检查分块文件是否存在
        //得到分块文件的所在目录
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        //通过存放分块文件的目录+文件下标。就指向这个分块文件
        File file = new File(chunkFileFolderPath + chunk);
        //判断有没有这个分块
        if (file.exists()){
            //如果有这个分块返回成功代码
            return new CheckChunkResult(CommonCode.SUCCESS,true);
        }else {
            //如果没有这个分块返回false
            return new CheckChunkResult(CommonCode.SUCCESS,false);
        }

    }

    /**
     * @param file  校验完成后，分块文件的内容
     * @param fileMd5   文件id
     * @param chunk     分块下标
     * @return  成功代码
     */
    public ResponseResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) {
        //先得到该文件的分块目录
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        //先判断有没有这个目录，没有就创建
        File folderPath = new File(chunkFileFolderPath);
        if (!folderPath.exists()){
            //没有，创建这个多级目录
            folderPath.mkdirs();
        }

        //建立目录+下标的，分块文件指向
        File chunkFile = new File(chunkFileFolderPath + chunk);
        //准备读流,上传的参数，自带路径。
        InputStream inputStream = null;
        //和File带路径的写流
        FileOutputStream outputStream=null;

        try {
            //获取读流文件,并赋值
            inputStream = file.getInputStream();
            //创建指向目录+文件id的写流，并赋值
            outputStream = new FileOutputStream(chunkFile);
            //把读到的流复制到写流中，完成上传块文件
            IOUtils.copy(inputStream,outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //最后关闭读写流
            try {
                inputStream.close();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //返回成功代码
        return new ResponseResult(CommonCode.SUCCESS);

    }
    /**
     *合并文件
     * @param fileMd5 用前端用MD5生成的文件id
     * @param fileName  文件名称
     * @param fileSize  文件大小
     * @param mimetype  文件属性
     * @param fileExt   文件扩展名
     * @return   返回成功代码
     */
    public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        //1、合并所有分块
        //得到分块文件的属目录
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        File chunkFile = new File(chunkFileFolderPath);
        //取分块文件列表，并转为list方便排序
        File[] files = chunkFile.listFiles();
        List<File> fileList = Arrays.asList(files);


        //准备一个合并后放文件的文件夹文件
        String filePath = this.getFilePath(fileMd5, fileExt);
        File mergeFile = new File(filePath);

        //执行合并,得到合併文件的指向
        File mergeFile1 = this.mergeFile(fileList, mergeFile);
        if (mergeFile1 == null) {
            //合併文件失敗
            ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
        }
        //執行文件校驗，File指向id和進來的id,是否一致
        boolean checkFileMd5 = this.checkFileMd5(mergeFile, fileMd5);
        if (!checkFileMd5){
            //校驗不成功，拋異常
            ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
        }
        //校驗成功，就把上傳的信息，給添加進mongo
        MediaFile mediaFile = new MediaFile();
        mediaFile.setFileId(fileMd5);
        mediaFile.setFileOriginalName(fileName);
        //文件名稱+擴展名
        mediaFile.setFileName(fileMd5 + "." +fileExt);
        //文件路径保存相对路径，也就是不要配置的前綴。
        String filePath1 = fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/";
        mediaFile.setFilePath(filePath1);
        mediaFile.setFileSize(fileSize);
        mediaFile.setUploadTime(new Date());
        mediaFile.setMimeType(mimetype);
        mediaFile.setFileType(fileExt);
        //状态为上传成功
        mediaFile.setFileStatus("301002");
        //執行保存
        mediaFileRepository.save(mediaFile);

        //向MQ发送视频处理消息
        this.sendProcessVideoMsg(mediaFile.getFileId());

        //返回成功代碼
        return new ResponseResult(CommonCode.SUCCESS);


    }


    /**
     * 发送视频处理消息
     * @param mediaId 文件id
     * @return
     */
    public ResponseResult sendProcessVideoMsg(String mediaId){
        //对进来的文件id先校验，数据库中有没有这个文件，只要上传成功了，就一定有这条信息
        Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
        //如果没有，抛异常
        if (!optional.isPresent()){
            ExceptionCast.cast(CommonCode.FAIL);
        }
        //如果有这个文件信息，就向mq发消息
        //提前准备好，文件id,的json串
        Map<String,String> map = new HashMap<>();
        map.put("mediaId",mediaId);
        //阿里的json工具类
        String jsonString = JSON.toJSONString(map);
        try {
            //向mq发消息,1.交换机  2.路由key 3.消息内容
            rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey,jsonString);
        } catch (AmqpException e) {
            e.printStackTrace();
            return new ResponseResult(CommonCode.FAIL);
        }
        return new ResponseResult(CommonCode.SUCCESS);

    }

    //校验文件
    private boolean checkFileMd5(File mergeFile,String md5){
        try {
            //創建文件的輸入流
            FileInputStream fileInputStream = new FileInputStream(mergeFile);
            //用阿帕奇的工具類得到MD5值
            String md5Hex = DigestUtils.md5Hex(fileInputStream);
            //對比md5值，不區分大小寫
            if (md5Hex.equalsIgnoreCase(md5)){
                //如果一致，就返回true
                return true;
            }
            //不一致，就false
            return false;

        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }


    //合并文件的方法，参数1：分块集合，参数2：存放合并文件的目录
    private File mergeFile(List<File> fileList,File mergeFile){

        //把整段代碼，都try異常
        try {
            //先对目录中的文件进行判断，有这个文件就删除覆盖
            if (mergeFile.exists()){
                //如果有这个文件，就删除
                mergeFile.delete();
            }else {
                //如果有就创建这个文件
                try {
                    mergeFile.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            //对分块文件List集合进行按名称排序
            Collections.sort(fileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    if (Integer.parseInt(o1.getName())>Integer.parseInt(o2.getName())){
                        return 1;
                    }
                    return -1;
                }
            });

            //排过序后，执行合并，遍历分块文件
            //先准备一个写流
            RandomAccessFile raf_rw = new RandomAccessFile(mergeFile,"rw");
            for (File file : fileList) {
                //每次循環準備一個讀流
                RandomAccessFile raf_r = new RandomAccessFile(file,"r");

                //準備一個緩存區
                byte[] bytes = new byte[1024];
                int len = -1;
                //讀流每次讀緩衝區大小的數據，只要有數據，就一直讀
                while ((len = (raf_r.read(bytes)))!=-1){
                    //讀一次，也就跟著寫一次
                    raf_rw.write(bytes,0,len);
                }
                //寫完一個塊文件，就關閉一次讀流
                raf_r.close();
            }
            //遍歷結束了，就關閉寫流
            raf_rw.close();
            //執行完，沒異常，就是合併成功,返回合併的文件File指向
            return mergeFile;
        } //整段try結束
        catch (IOException e) {
            e.printStackTrace();
            //有異常就返回null
            return null;
        }
    }
}

